package org.tulkas.log;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tulkas.base.PersistenceHelper;
import org.tulkas.config.GlobalConfig;
import org.tulkas.exception.SystemException;

import com.mchange.v2.c3p0.ComboPooledDataSource;

/**
 * @author <a href="mailto:tulkas.web@gmail.com">Tulkas</a>
 *
 */
public class LogManager {
	public static final String LOG_DB_DIR = "log";
	public static final String LOG_WORK_DIR = "_work";
	public static final int DEFAULT_QUERY_COUNT = 50;
	public static final int MAX_MESSAGE_LENGTH = 512;
	public static final int DEFAULT_LEVEL = LogRecord.LEVEL_INFO;

	private static Logger logger = LoggerFactory.getLogger(LogManager.class);
	private volatile static LogManager instance = null;

	private volatile boolean initialized = false;
	private volatile boolean systemInitialized = false;
	private volatile boolean forceJdkLogger = true;

	private LogDao workDao = null;
	private final Map<String, LogDao> archivedDaos = new HashMap<String, LogDao>();

	private int commonLevel = LogRecord.LEVEL_TRACE;
	private final Map<String, Integer> logLevels = new ConcurrentHashMap<String, Integer>();

	private LogManager() {
	}

	public static LogManager getInstance() {
		if (instance == null)
			synchronized (LogManager.class) {
				if (instance == null) {
					instance = new LogManager();
				}
			}
		return instance;
	}

	public static int getLevelInt(String levelString) {
		if ("error".equalsIgnoreCase(levelString)) {
			return LogRecord.LEVEL_ERROR;
		} else if ("warn".equalsIgnoreCase(levelString)) {
			return LogRecord.LEVEL_WARN;
		} else if ("info".equalsIgnoreCase(levelString)) {
			return LogRecord.LEVEL_INFO;
		} else if ("debug".equalsIgnoreCase(levelString)) {
			return LogRecord.LEVEL_DEBUG;
		} else if ("trace".equalsIgnoreCase(levelString)) {
			return LogRecord.LEVEL_TRACE;
		} else
			throw new SystemException("Unrecognizable log level string ["
					+ levelString + "]");
	}

	public static String getLevelString(int level) {
		switch (level) {
		case LogRecord.LEVEL_ERROR:
			return "ERROR";
		case LogRecord.LEVEL_WARN:
			return "WARN";
		case LogRecord.LEVEL_INFO:
			return "INFO";
		case LogRecord.LEVEL_DEBUG:
			return "DEBUG";
		case LogRecord.LEVEL_TRACE:
			return "TRACE";
		default:
			throw new SystemException("Inappropriate log level [" + level + "]");
		}
	}

	public boolean isInitialized() {
		return initialized;
	}

	public boolean isSystemInitialized() {
		return systemInitialized;
	}

	public boolean isForceJdkLogger() {
		return forceJdkLogger;
	}

	public void setForceJdkLogger(boolean forceJdkLogger) {
		this.forceJdkLogger = forceJdkLogger;
	}

	public void setSystemInitialized(boolean systemInitialized) {
		this.systemInitialized = systemInitialized;
	}

	public synchronized LogDao getArchivedDao(String archiveName) {
		LogDao dao = archivedDaos.get(archiveName);
		if (dao == null) {
			dao = initArchiveDao(archiveName);
			if (dao != null) {
				archivedDaos.put(archiveName, dao);
			}
		}
		if (dao == null) {
			throw new SystemException("No archive log database found for ["
					+ archiveName + "]");
		}
		return dao;
	}

	private LogDao initArchiveDao(String archiveName) {
		String dbPath = GlobalConfig.getInstance().getHomePath() + "/"
				+ LOG_DB_DIR + "/" + archiveName;
		boolean stat = PersistenceHelper.isDerbyDBExist(dbPath);
		if (stat) {
			logger.info("Found archive log database in [" + dbPath + "]");
		} else {
			logger.error("No archive log database in [" + dbPath + "]!");
			return null;
		}

		GlobalConfig cfg = GlobalConfig.getInstance();
		logger.info("Initialize archive log data source.");
		Properties props = cfg.getSubProperties("logger.archive.datasource.");
		props.setProperty("jdbcUrl", PersistenceHelper.getDerbyJdbcUrl(dbPath));
		ComboPooledDataSource datasource = new ComboPooledDataSource();

		PersistenceHelper.initC3p0DataSource(datasource, props);
		logger.trace("Archive log datasource: {}", datasource);

		LogDao dao = new LogDao(datasource);
		return dao;
	}

	private void initDefaultDao() {
		String dbPath = GlobalConfig.getInstance().getHomePath() + "/"
				+ LOG_DB_DIR + "/" + LOG_WORK_DIR;
		boolean stat = PersistenceHelper.ensureDerbyDBCreated(dbPath);
		if (stat) {
			logger.info("Found log database in [" + dbPath + "]");
		} else {
			logger.info("Log database not exist in [" + dbPath
					+ "]. Create it.");
		}

		GlobalConfig cfg = GlobalConfig.getInstance();

		logger.info("Initialize log data source.");
		Properties props = cfg.getSubProperties("logger.datasource.");
		props.setProperty("jdbcUrl", PersistenceHelper.getDerbyJdbcUrl(dbPath));
		ComboPooledDataSource datasource = new ComboPooledDataSource();

		PersistenceHelper.initC3p0DataSource(datasource, props);
		logger.trace("Log datasource: {}", datasource);

		workDao = new LogDao(datasource);
		if (!workDao.isTablesExist()) {
			logger.info("Tulkas log tables not existing, create log tables");
			workDao.dropTables();
			workDao.createTables();
		}
	}

	public void init() {
		logger.info("Tulkas log intialization start.");
		initDefaultDao();

		GlobalConfig cfg = GlobalConfig.getInstance();
		String prefix = "logger.level.";
		String[] keys = cfg.getAllKeys(prefix);
		for (String key : keys) {
			String type = key.substring(prefix.length());
			if ("common".equals(type)) {
				setLogLevel(null, getLevelInt(cfg.getString(key)));
			} else {
				setLogLevel(type, getLevelInt(cfg.getString(key)));
			}
		}

		initialized = true;
		logger.info("Tulkas log intialization done.");
		logger.trace("{}", this);
	}

	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append("Tulkas LogManager: ");
		sb.append("initialized->" + initialized);
		sb.append(", systemInitialized->" + systemInitialized);
		sb.append(", forceJdkLogger->" + forceJdkLogger);
		sb.append(", levels->[ common:" + getLevelString(commonLevel));
		for (Entry<String, Integer> entry : logLevels.entrySet()) {
			sb.append(", ");
			sb.append(entry.getKey() + ":" + getLevelString(entry.getValue()));
		}
		sb.append("]");

		return sb.toString();
	}

	public void setLogLevel(String logType, int level) {
		if (logType == null) {
			commonLevel = level;
		} else {
			logLevels.put(logType, level);
		}
	}

	public int getLoggerLevel(String logType) {
		if (logType == null) {
			return commonLevel;
		} else {
			Integer level = logLevels.get(logType);
			if (level != null) {
				return level;
			} else {
				return DEFAULT_LEVEL;
			}
		}
	}

	public void log(LogRecord record) {
		workDao.insertLog(record);
	}

	public void remove(LogQuery query) {
		workDao.removeLogs(query);
	}

	public String getStackTrace(long id) {
		return workDao.getStaceTraceById(id);
	}

	public LogRecord[] query(LogQuery query, int count) {
		return workDao.queryLogs(query, count);
	}

	public LogRecord[] query(LogQuery query) {
		return workDao.queryLogs(query, DEFAULT_QUERY_COUNT);
	}

	public void archive(String archiveName) {
		File toFile = new File(GlobalConfig.getInstance().getHomePath() + "/"
				+ LOG_DB_DIR + "/" + archiveName);
		if (toFile.exists()) {
			throw new SystemException("Archive already exists [" + archiveName
					+ "]");
		}
		this.initialized = false;
		this.forceJdkLogger = true;
		workDao.destroy();

		try {
			File fromFile = new File(GlobalConfig.getInstance().getHomePath()
					+ "/" + LOG_DB_DIR + "/" + LOG_WORK_DIR);
			if (!fromFile.renameTo(toFile)) {
				throw new SystemException(
						"Archive failed! Cannot rename work folder to ["
								+ archiveName + "]");
			}
		} finally {
			initDefaultDao();
			this.forceJdkLogger = false;
			initialized = true;
		}
	}

	public String[] getAllArchives() {
		File dir = new File(GlobalConfig.getInstance().getHomePath() + "/"
				+ LOG_DB_DIR);
		File[] files = dir.listFiles();
		List<String> retList = new ArrayList<String>();
		for (File file : files) {
			if (file.isDirectory()) {
				retList.add(file.getName());
			}
		}
		return retList.toArray(new String[retList.size()]);
	}

	public void removeArchive(String archiveName, LogQuery query) {
		getArchivedDao(archiveName).removeLogs(query);
	}

	public String getArchiveStackTrace(String archiveName, long id) {
		return getArchivedDao(archiveName).getStaceTraceById(id);
	}

	public LogRecord[] queryArchive(String archiveName, LogQuery query,
			int count) {
		return getArchivedDao(archiveName).queryLogs(query, count);
	}

	public LogRecord[] queryArchive(String archiveName, LogQuery query) {
		return getArchivedDao(archiveName)
				.queryLogs(query, DEFAULT_QUERY_COUNT);
	}
}
